<!--
 * @Author: wanlixin
 * @Date: 2019-10-05 10:41:31
 * @LastEditors: sylvanas
 * @LastEditTime: 2019-10-29 21:21:42
 * @Description: 
 -->
<template>
  <div ref="wrapper" class="grimm-scroll-wrapper">
    <div class="grimm-scroll-content">
      <div ref="listWrapper" class="grimm-scroll-list-wrapper">
        <slot>
          <ul class="grimm-scroll-list">
            <li
              class="grimm-scroll-item border-bottom-1px"
              v-for="(item, index) in data"
              :key="index"
              @click="clickItem(item)"
            >{{item}}</li>
          </ul>
        </slot>
      </div>
      <slot name="pullup" :pullUpLoad="pullUpLoad" :isPullUpLoad="isPullUpLoad">
        <div class="grimm-pullup-wrapper" v-if="pullUpLoad">
          <div class="before-trigger" v-if="!isPullUpLoad">
            <span>{{ pullUpTxt }}</span>
          </div>
          <div class="after-trigger" v-else>
            <loading></loading>
          </div>
        </div>
      </slot>
    </div>
    <div v-if="pullDownRefresh" class="grimm-pulldown" ref="pulldown">
      <slot
        name="pulldown"
        :pullDownRefresh="pullDownRefresh"
        :pullDownStyle="pullDownStyle"
        :beforePullDown="beforePullDown"
        :isPullingDown="isPullingDown"
        :bubbleY="bubbleY"
      >
        <div class="grimm-pulldown-wrapper" :style="pullDownStyle">
          <div class="before-trigger" v-show="beforePullDown">
            <bubble :y="bubbleY" class="bubble"></bubble>
          </div>
          <div class="after-trigger" v-show="!beforePullDown">
            <div v-show="isPullingDown" class="loading">
              <loading></loading>
            </div>
            <div v-show="!isPullingDown" class="grimm-pulldown-loaded">
              <span>{{ refreshTxt }}</span>
            </div>
          </div>
        </div>
      </slot>
    </div>
  </div>
</template>

<script type="text/ecmascript-6">
import BScroll from 'better-scroll';
import Loading from '../../components/Loading';
import Bubble from '../../components/Bubble/bubble.vue';
import scrollMixin from '../../common/mixins/scroll';
import deprecatedMixin from '../../common/mixins/deprecated';
import { getRect } from '../../common/helpers/dom';
import { camelize } from '../../common/lang/string';

const COMPONENT_NAME = 'grimm-scroll';
const DIRECTION_H = 'horizontal';
const DIRECTION_V = 'vertical';
const DEFAULT_REFRESH_TXT = 'Refresh success';
const DEFAULT_STOP_TIME = 600;

const EVENT_CLICK = 'click';
const EVENT_PULLING_DOWN = 'pulling-down';
const EVENT_PULLING_UP = 'pulling-up';

const EVENT_SCROLL = 'scroll';
const EVENT_BEFORE_SCROLL_START = 'before-scroll-start';
const EVENT_SCROLL_END = 'scroll-end';

const NEST_MODE_NONE = 'none';
const NEST_MODE_NATIVE = 'native';

const SCROLL_EVENTS = [EVENT_SCROLL, EVENT_BEFORE_SCROLL_START, EVENT_SCROLL_END];

const DEFAULT_OPTIONS = {
  observeDOM: true,
  click: true,
  probeType: 1,
  scrollbar: false,
  pullDownRefresh: false,
  pullUpLoad: false,
};

export default {
  name: COMPONENT_NAME,
  mixins: [scrollMixin, deprecatedMixin],
  provide() {
    return {
      parentScroll: this,
    };
  },
  inject: {
    parentScroll: {
      default: null,
    },
  },
  props: {
    data: {
      type: Array,
      default() {
        return [];
      },
    },
    scrollEvents: {
      type: Array,
      default() {
        return [];
      },
      validator(arr) {
        return arr.every(item => {
          return SCROLL_EVENTS.indexOf(item) !== -1;
        });
      },
    },
    // TODO: plan to remove at 1.10.0
    listenScroll: {
      type: Boolean,
      default: undefined,
      deprecated: {
        replacedBy: 'scroll-events',
      },
    },
    listenBeforeScroll: {
      type: Boolean,
      default: undefined,
      deprecated: {
        replacedBy: 'scroll-events',
      },
    },
    direction: {
      type: String,
      default: DIRECTION_V,
    },
    refreshDelay: {
      type: Number,
      default: 20,
    },
    nestMode: {
      type: String,
      default: NEST_MODE_NONE,
    },
  },
  data() {
    return {
      beforePullDown: true,
      isPullingDown: false,
      isPullUpLoad: false,
      pullUpNoMore: false,
      bubbleY: 0,
      pullDownStyle: '',
      pullDownStop: 40,
      pullDownHeight: 60,
      pullUpHeight: 0,
    };
  },
  computed: {
    pullDownRefresh() {
      let { pullDownRefresh } = this.options;
      if (!pullDownRefresh) {
        return pullDownRefresh;
      }
      if (pullDownRefresh === true) {
        pullDownRefresh = {};
      }
      return Object.assign({ stop: this.pullDownStop }, pullDownRefresh);
    },
    pullUpLoad() {
      return this.options.pullUpLoad;
    },
    pullUpTxt() {
      const { pullUpLoad } = this;
      const txt = pullUpLoad && pullUpLoad.txt;
      const moreTxt = (txt && txt.more) || '';
      const noMoreTxt = (txt && txt.noMore) || '';

      return this.pullUpNoMore ? noMoreTxt : moreTxt;
    },
    refreshTxt() {
      const { pullDownRefresh } = this;
      return (pullDownRefresh && pullDownRefresh.txt) || DEFAULT_REFRESH_TXT;
    },
    finalScrollEvents() {
      const finalScrollEvents = this.scrollEvents.slice();

      if (!finalScrollEvents.length) {
        this.listenScroll && finalScrollEvents.push(EVENT_SCROLL);
        this.listenBeforeScroll && finalScrollEvents.push(EVENT_BEFORE_SCROLL_START);
      }
      return finalScrollEvents;
    },
    needListenScroll() {
      return this.finalScrollEvents.indexOf(EVENT_SCROLL) !== -1 || this.parentScroll;
    },
  },
  watch: {
    data() {
      setTimeout(() => {
        this.forceUpdate(true);
      }, this.refreshDelay);
    },
    pullDownRefresh: {
      handler(newVal, oldVal) {
        if (newVal) {
          this.scroll.openPullDown(newVal);
          if (!oldVal) {
            this._onPullDownRefresh();
            this._pullDownRefreshChangeHandler();
          }
        }

        if (!newVal && oldVal) {
          this.scroll.closePullDown();
          this._offPullDownRefresh();
          this._pullDownRefreshChangeHandler();
        }
      },
      deep: true,
    },
    pullUpLoad: {
      handler(newVal, oldVal) {
        if (newVal) {
          this.scroll.openPullUp(newVal);
          if (!oldVal) {
            this._onPullUpLoad();
            this._pullUpLoadChangeHandler();
          }
        }

        if (!newVal && oldVal) {
          this.scroll.closePullUp();
          this._offPullUpLoad();
          this._pullUpLoadChangeHandler();
        }
      },
      deep: true,
    },
  },
  activated() {
    /* istanbul ignore next */
    this.enable();
  },
  deactivated() {
    /* istanbul ignore next */
    this.disable();
  },
  mounted() {
    this.$nextTick(() => {
      this.initScroll();
    });
  },
  beforeDestroy() {
    this.destroy();
  },
  methods: {
    initScroll() {
      if (!this.$refs.wrapper) {
        return;
      }
      this._calculateMinHeight();

      let options = Object.assign(
        {},
        DEFAULT_OPTIONS,
        {
          scrollY: this.direction === DIRECTION_V,
          scrollX: this.direction === DIRECTION_H,
          probeType: this.needListenScroll ? 3 : 1,
        },
        this.options
      );

      this.scroll = new BScroll(this.$refs.wrapper, options);

      this.parentScroll && this.nestMode !== NEST_MODE_NONE && this._handleNestScroll();

      this._listenScrollEvents();

      if (this.pullDownRefresh) {
        this._onPullDownRefresh();
        this._pullDownRefreshChangeHandler();
      }

      if (this.pullUpLoad) {
        this._onPullUpLoad();
        this._pullUpLoadChangeHandler();
      }
    },
    disable() {
      this.scroll && this.scroll.disable();
    },
    enable() {
      this.scroll && this.scroll.enable();
    },
    refresh() {
      this._calculateMinHeight();
      this.scroll && this.scroll.refresh();
    },
    destroy() {
      this.scroll && this.scroll.destroy();
      this.scroll = null;
    },
    scrollTo() {
      this.scroll && this.scroll.scrollTo.apply(this.scroll, arguments);
    },
    scrollToElement() {
      this.scroll && this.scroll.scrollToElement.apply(this.scroll, arguments);
    },
    clickItem(item) {
      this.$emit(EVENT_CLICK, item);
    },
    async forceUpdate(dirty = false, nomore = false) {
      if (this.isPullDownUpdating) {
        return;
      }

      if (this.pullDownRefresh && this.isPullingDown) {
        this.isPullingDown = false;
        this.isPullDownUpdating = true;
        await this._waitFinishPullDown();
        this.isPullDownUpdating = false;
        await this._waitResetPullDown(dirty);
      } else if (this.pullUpLoad && this.isPullUpLoad) {
        this.isPullUpLoad = false;
        this.scroll.finishPullUp();
        this.pullUpNoMore = !dirty || nomore;
      }

      dirty && this.refresh();
    },
    resetPullUpTxt() {
      this.pullUpNoMore = false;
    },
    _listenScrollEvents() {
      this.finalScrollEvents.forEach(event => {
        this.scroll.on(camelize(event), (...args) => {
          this.$emit(event, ...args);
        });
      });
    },
    _handleNestScroll() {
      // waiting scroll initial
      this.$nextTick(() => {
        const innerScroll = this.scroll;
        const outerScroll = this.parentScroll.scroll;
        const scrolls = [innerScroll, outerScroll];
        scrolls.forEach((scroll, index, arr) => {
          // scroll ended always enable them
          scroll.on('touchEnd', () => {
            outerScroll.enable();
            innerScroll.enable();
            // when inner scroll reaching boundary, we will disable inner scroll, so when 'touchend' event fire,
            // the inner scroll will not reset initiated within '_end' method in better-scroll.
            // then lead to inner and outer scrolls together when we touch and move on the outer scroll element,
            // so here we reset inner scroll's 'initiated' manually.
            innerScroll.initiated = false;
          });

          scroll.on('beforeScrollStart', () => {
            this.touchStartMoment = true;
            const anotherScroll = arr[(index + 1) % 2];
            anotherScroll.stop();
            anotherScroll.resetPosition();
          });
        });

        innerScroll.on('scroll', pos => {
          // if scroll event triggered not by touch event, such as by 'scrollTo' method
          if (!innerScroll.initiated || innerScroll.isInTransition) {
            return;
          }

          if (this.nestMode === NEST_MODE_NATIVE && !this.touchStartMoment) {
            return;
          }

          const reachBoundary = this._checkReachBoundary(pos);
          if (reachBoundary) {
            innerScroll.resetPosition();
            innerScroll.disable();
            // Prevent outer scroll have a bouncing step when enabled in 'free' nestMode.
            outerScroll.pointX = innerScroll.pointX;
            outerScroll.pointY = innerScroll.pointY;
            outerScroll.enable();
          } else {
            outerScroll.disable();
          }
          this.touchStartMoment = false;
        });
      });
    },
    _checkReachBoundary(pos) {
      const { distX, distY } = this.scroll.distY;
      const reachBoundaryX =
        distX > 0
          ? pos.x >= this.scroll.minScrollX
          : distX < 0
          ? pos.x <= this.scroll.maxScrollX
          : false;
      const reachBoundaryY =
        distY > 0
          ? pos.y >= this.scroll.minScrollY
          : distY < 0
          ? pos.y <= this.scroll.maxScrollY
          : false;
      const { freeScroll } = this.scroll;

      const { hasHorizontalScroll, hasVerticalScroll } = this.scroll;

      if (!hasHorizontalScroll && !hasVerticalScroll) {
        return true;
      }

      if (freeScroll) {
        return reachBoundaryX || reachBoundaryY;
      }

      let reachBoundary;
      if (this.scroll.movingDirectionX) {
        reachBoundary = reachBoundaryX;
      } else if (this.scroll.movingDirectionY) {
        reachBoundary = reachBoundaryY;
      }
      return reachBoundary;
    },
    _calculateMinHeight() {
      const { wrapper, listWrapper } = this.$refs;
      const { pullUpLoad, pullDownRefresh } = this;
      let minHeight = 0;

      if (pullDownRefresh || pullUpLoad) {
        const wrapperHeight = getRect(wrapper).height;
        minHeight = wrapperHeight + 1;
        if (pullUpLoad && pullUpLoad.visible) {
          // minHeight = wrapperHeight + 1 - pullUpHeight, then pullUpLoad text is visible
          // when content's height is not larger than wrapper height
          minHeight -= this.pullUpHeight;
        }
      }

      listWrapper.style.minHeight = `${minHeight}px`;
    },
    _onPullDownRefresh() {
      this.scroll.on('pullingDown', this._pullDownHandle);
      this.scroll.on('scroll', this._pullDownScrollHandle);
    },
    _offPullDownRefresh() {
      this.scroll.off('pullingDown', this._pullDownHandle);
      this.scroll.off('scroll', this._pullDownScrollHandle);
    },
    _pullDownRefreshChangeHandler() {
      this.$nextTick(() => {
        this._getPullDownEleHeight();
        this._calculateMinHeight();
      });
    },
    _pullDownHandle() {
      if (this.resetPullDownTimer) {
        clearTimeout(this.resetPullDownTimer);
      }
      this.beforePullDown = false;
      this.isPullingDown = true;
      this.$emit(EVENT_PULLING_DOWN);
    },
    _pullDownScrollHandle(pos) {
      if (this.beforePullDown) {
        this.bubbleY = Math.max(0, pos.y - this.pullDownHeight);
        this.pullDownStyle = `top:${Math.min(pos.y - this.pullDownHeight, 0)}px`;
      } else {
        this.bubbleY = 0;
        this.pullDownStyle = `top:${Math.min(pos.y - this.pullDownStop, 0)}px`;
      }
    },
    _pullUpLoadChangeHandler() {
      this.$nextTick(() => {
        this._getPullUpEleHeight();
        this._calculateMinHeight();
      });
    },
    _onPullUpLoad() {
      this.scroll.on('pullingUp', this._pullUpHandle);
    },
    _offPullUpLoad() {
      this.scroll.off('pullingUp', this._pullUpHandle);
    },
    _pullUpHandle() {
      this.isPullUpLoad = true;
      this.$emit(EVENT_PULLING_UP);
    },
    _waitFinishPullDown(next) {
      const { stopTime = DEFAULT_STOP_TIME } = this.pullDownRefresh;
      return new Promise(resolve => {
        setTimeout(() => {
          this.scroll.finishPullDown();
          resolve();
        }, stopTime);
      });
    },
    _waitResetPullDown(dirty) {
      return new Promise(resolve => {
        this.resetPullDownTimer = setTimeout(() => {
          this.pullDownStyle = `top: -${this.pullDownHeight}px`;
          this.beforePullDown = true;
          resolve();
        }, this.scroll.options.bounceTime);
      });
    },
    _getPullDownEleHeight() {
      let { pulldown } = this.$refs;
      if (!pulldown) {
        return;
      }
      pulldown = pulldown.firstChild;
      this.pullDownHeight = getRect(pulldown).height;

      this.beforePullDown = false;
      this.isPullingDown = true;
      this.$nextTick(() => {
        this.pullDownStop = getRect(pulldown).height;

        this.beforePullDown = true;
        this.isPullingDown = false;
      });
    },
    _getPullUpEleHeight() {
      const {
        listWrapper: { pullup },
      } = this.$refs.listWrapper;
      if (!pullup) {
        this.pullUpHeight = 0;
        return;
      }
      this.pullUpHeight = getRect(pullup).height;
    },
  },
  components: {
    Loading,
    Bubble,
  },
};
</script>

<style lang="stylus" >
@import '../../common/stylus/variable.styl';
.grimm-scroll-wrapper {
  position: relative;
  height: 100%;
  overflow: hidden;
}

.grimm-scroll-list-wrapper {
  overflow: hidden;
}

.grimm-pulldown-wrapper {
  position: absolute;
  width: 100%;
  left: 0;
  display: flex;
  justify-content: center;
  align-items: center;
  transition: all;
  .before-trigger {
    height: 54px;
    line-height: 0;
    padding-top: 6px;
  }
  .after-trigger {
    .loading {
      padding: 8px 0;
    }
    .grimm-pulldown-loaded {
      padding: 12px 0;
    }
  }
}

.grimm-pullup-wrapper {
  width: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  .before-trigger {
    padding: 22px 0;
    min-height: 1em;
  }
  .after-trigger {
    padding: 19px 0;
  }
}

.grimm-scroll-content {
  position: relative;
  z-index: 1;
}

.grimm-scroll-item {
  height: 60px;
  line-height: 60px;
  // font-size: $fontsize-large;
  padding-left: 20px;
}
</style>